package com.lemon.exam.common.filter;

import com.lemon.exam.common.constant.RedisKeyConst;
import com.lemon.exam.common.entity.system.LoginUser;
import com.lemon.exam.common.enums.CodeEnum;
import com.lemon.exam.common.exception.CustomException;
import com.lemon.exam.common.properties.ConfigProperties;
import com.lemon.exam.common.redis.RedisService;
import com.lemon.exam.common.util.JwtUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.PathContainer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;
import reactor.core.publisher.Mono;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

/**
 * JWT 认证过滤器
 *
 * @author Lemon
 * @since 2025/3/21 18:31
 */
@Slf4j
@Component
public class JwtAuthenticationFilter implements WebFilter {
    private final List<PathPattern> ignorePatterns;
    private final String accessTokenPrefix;
    private final Map<String, Boolean> pathCache = new ConcurrentHashMap<>();

    @Resource
    private JwtUtil jwtUtil;
    @Resource
    private RedisService redisService;

    public JwtAuthenticationFilter(ConfigProperties configProperties) {
        PathPatternParser parser = new PathPatternParser();
        this.ignorePatterns = configProperties.getAllIgnoreUri().parallelStream().map(parser::parse).toList();
        this.accessTokenPrefix = configProperties.getJwt().getAccessTokenPrefix();
    }

    @Override
    @NonNull
    public Mono<Void> filter(ServerWebExchange exchange, @NonNull WebFilterChain chain) {
        //1.放行白名单路径
        if (isIgnoredPath(exchange.getRequest())) {
            return chain.filter(exchange);
        }
        //2.获取并验证Token
        return extractToken(exchange.getRequest())
                .flatMap(this::validateToken)
                .flatMap(claims -> loadUserDetails(claims)
                        .flatMap(loginUser -> authenticateAndProceed(exchange, chain, loginUser))
                        .onErrorResume(Mono::error));
    }

    /**
     * 判断是否为白名单路径
     *
     * @param request
     * @return
     */
    private boolean isIgnoredPath(ServerHttpRequest request) {
        return pathCache.computeIfAbsent(request.getURI().getPath(), path -> ignorePatterns.parallelStream()
                .anyMatch(pattern -> pattern.matches(PathContainer.parsePath(path)))
        );
    }

    /**
     * 提取Token
     *
     * @param request
     * @return
     */
    private Mono<String> extractToken(ServerHttpRequest request) {
        return Mono.fromCallable(() -> {
            String token = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);

            if (!StringUtils.hasText(token) || !token.startsWith(this.accessTokenPrefix)) {
                throw new CustomException(401, "暂无认证信息");
            }
            return token.replaceFirst(this.accessTokenPrefix, "");
        });
    }

    /**
     * 验证Token
     *
     * @param token
     * @return
     */
    private Mono<Jws<Claims>> validateToken(String token) {
        try {
            return Mono.just(jwtUtil.parseToken(token));
        } catch (ExpiredJwtException e) {
            //这个异常是需要刷新的
            return Mono.error(new CustomException(CodeEnum.TOKEN_REFRESH));
        } catch (Exception e) {
            return Mono.error(new CustomException(401, "无效的Token"));
        }
    }

    /**
     * 加载用户详情
     *
     * @param claims
     * @return
     */
    private Mono<LoginUser> loadUserDetails(Jws<Claims> claims) {
        return redisService.hash().get(RedisKeyConst.LOGIN_USER, jwtUtil.getUsername(claims))
                .switchIfEmpty(Mono.error(new CustomException(401, "暂无认证信息")))
                .map(TO_LOGIN_USER);
    }

    /**
     * 认证并继续执行
     *
     * @param exchange
     * @param chain
     * @param loginUser
     * @return
     */
    private Mono<Void> authenticateAndProceed(ServerWebExchange exchange, WebFilterChain chain, LoginUser loginUser) {
        return chain.filter(exchange)
                .contextWrite(ReactiveSecurityContextHolder.withAuthentication(new UsernamePasswordAuthenticationToken(
                        loginUser,
                        loginUser.getPassword(),
                        loginUser.getAuthorities()
                )));
    }

    /**
     * 登录用户转换器
     */
    private static final Function<Object, LoginUser> TO_LOGIN_USER = object -> {
        LoginUser loginUser = new LoginUser();
        if (!(object instanceof LinkedHashMap<?, ?> map)) {
            return loginUser;
        }
        Optional.ofNullable(map.get("id")).ifPresent(id -> loginUser.setId(Long.valueOf(String.valueOf(id))));
        Optional.ofNullable(map.get("username")).ifPresent(username -> loginUser.setUsername(String.valueOf(username)));
        Optional.ofNullable(map.get("name")).ifPresent(name -> loginUser.setName(String.valueOf(name)));
        Optional.ofNullable(map.get("password")).ifPresent(password -> loginUser.setPassword(String.valueOf(password)));
        Optional.ofNullable(map.get("enabled")).ifPresent(enabled -> loginUser.setEnabled(Boolean.TRUE.equals(enabled)));
        Optional.ofNullable(map.get("roleList")).ifPresent(roleList -> {
            if (roleList instanceof ArrayList<?> list && list.size() != 0) {
                loginUser.setRoleList(list.parallelStream().map(Object::toString).toList());
            }
        });
        Optional.ofNullable(map.get("resourceList")).ifPresent(resourceList -> {
            if (resourceList instanceof ArrayList<?> list && list.size() != 0) {
                loginUser.setResourceList(list.parallelStream().map(Object::toString).toList());
            }
        });
        return loginUser;
    };
}
